"""
配置管理服务
支持从Gitee和GitHub + jsDelivr CDN拉取学校配置和Docker Compose文件
"""

import json
import logging
import os
import re
import requests
from pathlib import Path
from typing import Dict, Any, Optional, Tuple
from urllib.parse import quote

logger = logging.getLogger(__name__)

class ConfigService:
    """统一配置服务类，支持Gitee和GitHub + jsDelivr CDN"""
    
    def __init__(self, source_type: str = "github", repo_url: str = None, access_token: str = None):
        """
        初始化配置服务
        
        Args:
            source_type: 配置源类型 ('gitee' 或 'github')
            repo_url: 仓库URL
            access_token: 访问令牌（可选）
        """
        self.source_type = source_type.lower()
        self.access_token = access_token or os.getenv('GITEE_ACCESS_TOKEN', '')
        self.config_cache_dir = Path('/tmp/gitee_configs')
        self.config_cache_dir.mkdir(exist_ok=True)
        
        if self.source_type == "github":
            self.repo_url = repo_url or os.getenv('GITHUB_REPO_URL', 'https://github.com/haitao926/mobox_dashboard')
            self._parse_github_repo_info()
            # 允许通过环境变量覆盖分支/标签与CDN基域
            self.github_ref = os.getenv('GITHUB_REF', 'main')
            # 支持多个CDN基域，逗号分隔
            bases = os.getenv('JSDELIVR_BASES', '') or os.getenv('JSDELIVR_BASE', 'https://cdn.jsdelivr.net')
            self.jsdelivr_bases = [b.strip() for b in bases.split(',') if b.strip()]
            if not self.jsdelivr_bases:
                self.jsdelivr_bases = ['https://cdn.jsdelivr.net', 'https://fastly.jsdelivr.net', 'https://gcore.jsdelivr.net']
            # 记录候选CDN
            logger.info(f"jsDelivr CDN 候选: {self.jsdelivr_bases}")
        else:
            self.repo_url = repo_url or os.getenv('GITEE_REPO_URL', 'https://gitee.com/wht0926/mobox_dashboard')
            self._parse_gitee_repo_info()
        
        if not self.repo_url:
            raise ValueError("仓库URL未配置")
            
        logger.info(f"配置服务初始化完成: {self.source_type} - {self.username}/{self.repo_name}")
    
    def _parse_github_repo_info(self):
        """解析GitHub仓库信息"""
        pattern = r'https://github\.com/([^/]+)/([^/]+?)(?:\.git)?/?$'
        match = re.match(pattern, self.repo_url)
        
        if not match:
            raise ValueError(f"无效的GitHub仓库URL: {self.repo_url}")
        
        self.username = match.group(1)
        self.repo_name = match.group(2)
        
        # GitHub API基础URL
        self.api_base_url = f"https://api.github.com/repos/{self.username}/{self.repo_name}"
        
        logger.info(f"解析GitHub仓库: {self.username}/{self.repo_name}")
    
    def _parse_gitee_repo_info(self):
        """解析Gitee仓库信息"""
        pattern = r'https://gitee\.com/([^/]+)/([^/]+?)(?:\.git)?/?$'
        match = re.match(pattern, self.repo_url)
        
        if not match:
            raise ValueError(f"无效的Gitee仓库URL: {self.repo_url}")
        
        self.username = match.group(1)
        self.repo_name = match.group(2)
        
        # Gitee API基础URL
        self.api_base_url = f"https://gitee.com/api/v5/repos/{self.username}/{self.repo_name}"
        
        logger.info(f"解析Gitee仓库: {self.username}/{self.repo_name}")
    
    def _normalize_school_name(self, school_name: str) -> str:
        """
        标准化学校名称为文件名安全的格式
        
        Args:
            school_name: 原始学校名称，如"清华大学"
            
        Returns:
            标准化的学校代码，如"tsinghua"
        """
        # 这里可以根据实际需要实现学校名称到代码的映射
        # 简单实现：移除空格，转为拼音或英文缩写
        school_mapping = {
            '测试学校': 'testschool',
            '清华大学': 'tsinghua',
            '北京大学': 'pku',
            # 可以继续添加更多映射
        }
        
        normalized = school_mapping.get(school_name)
        if normalized:
            return normalized
        
        # 如果没有预定义映射，使用简单的转换规则
        # 移除空格和特殊字符，转为小写
        normalized = re.sub(r'[^\w\u4e00-\u9fff]', '', school_name).lower()
        return normalized
    
    def _get_file_content(self, file_path: str) -> str:
        """
        获取文件内容（支持Gitee和GitHub + jsDelivr）
        
        Args:
            file_path: 文件路径，如 "config/测试学校.json"
            
        Returns:
            文件内容
        """
        if self.source_type == "github":
            # 优先使用 jsDelivr，失败则回退到 GitHub Raw
            try:
                return self._get_file_content_via_jsdelivr(file_path)
            except Exception as cdn_error:
                logger.warning(f"jsDelivr失败，回退到GitHub Raw: {cdn_error}")
                try:
                    return self._get_file_content_via_raw(file_path)
                except Exception as raw_error:
                    logger.error(f"GitHub Raw获取也失败: {raw_error}")
                    # 解码URL用于显示更友好的错误信息
                    from urllib.parse import unquote
                    display_path = unquote(file_path)
                    raise Exception(f"无法获取文件 {display_path}: CDN错误={cdn_error}, Raw错误={raw_error}")
        else:
            # 原有的Gitee逻辑
            try:
                return self._get_file_content_via_api(file_path)
            except Exception as api_error:
                logger.warning(f"API方式获取文件失败: {api_error}")
                
                # 备用方案：使用raw URL
                try:
                    return self._get_file_content_via_raw(file_path)
                except Exception as raw_error:
                    logger.error(f"Raw方式也失败: {raw_error}")
                    # 解码URL用于显示更友好的错误信息
                    from urllib.parse import unquote
                    display_path = unquote(file_path)
                    raise Exception(f"无法获取文件 {display_path}: API错误={api_error}, Raw错误={raw_error}")
    
    def _get_file_content_via_jsdelivr(self, file_path: str) -> str:
        """通过 jsDelivr CDN 获取GitHub文件内容（支持多个CDN域名轮询）"""
        last_exc = None
        for base in self.jsdelivr_bases:
            try:
                # 对中文/特殊字符进行URL编码
                encoded_path = quote(file_path, safe='/')
                url = f"{base}/gh/{self.username}/{self.repo_name}@{self.github_ref}/{encoded_path}"
                logger.info(f"尝试通过jsDelivr获取文件: {url}")
                response = requests.get(url, timeout=30)
                response.raise_for_status()
                logger.info(f"成功通过jsDelivr获取文件: {file_path}")
                return response.text
            except requests.exceptions.RequestException as e:
                last_exc = e
                status = getattr(e.response, 'status_code', None) if hasattr(e, 'response') and e.response is not None else None
                logger.warning(f"jsDelivr候选{base}失败 {file_path}: {e} (status={status})，尝试下一个")
                continue
        # 所有CDN均失败
        if last_exc is not None:
            status = getattr(last_exc.response, 'status_code', None) if hasattr(last_exc, 'response') and last_exc.response is not None else None
            raise Exception(f"无法通过jsDelivr获取文件 {file_path}: 最后错误={last_exc} (status={status})")
        raise Exception(f"无法通过jsDelivr获取文件 {file_path}: 未知错误")
    
    def _get_file_content_via_raw(self, file_path: str) -> str:
        """通过Raw URL获取文件内容（Gitee备用方案）"""
        try:
            encoded_path = quote(file_path, safe='/')
            if self.source_type == "github":
                # GitHub raw URL
                raw_url = f"https://raw.githubusercontent.com/{self.username}/{self.repo_name}/main/{encoded_path}"
            else:
                # Gitee raw URL
                raw_url = f"https://gitee.com/{self.username}/{self.repo_name}/raw/master/{encoded_path}"
            
            logger.info(f"尝试Raw URL获取: {raw_url}")
            
            response = requests.get(raw_url, timeout=30)
            response.raise_for_status()
            
            content = response.text
            logger.info(f"Raw URL获取成功: {file_path}")
            return content
            
        except Exception as e:
            logger.error(f"Raw URL获取失败 {file_path}: {e}")
            raise
    
    def _get_file_content_via_api(self, file_path: str) -> str:
        """通过API获取文件内容（仅适用于Gitee）"""
        # 构建API URL
        encoded_path = quote(file_path, safe='/')
        url = f"{self.api_base_url}/contents/{encoded_path}"
        
        # 添加访问令牌（如果有）
        params = {}
        if self.access_token:
            params['access_token'] = self.access_token
        
        try:
            response = requests.get(url, params=params, timeout=30)
            response.raise_for_status()
            
            data = response.json()
            
            # Gitee API返回base64编码的内容
            import base64
            content = base64.b64decode(data['content']).decode('utf-8')
            
            logger.info(f"成功获取文件内容: {file_path}")
            return content
            
        except requests.exceptions.RequestException as e:
            logger.error(f"获取文件失败 {file_path}: {e}")
            raise
        except (KeyError, ValueError) as e:
            logger.error(f"解析文件内容失败 {file_path}: {e}")
            raise
    
    def _cache_file(self, file_path: str, content: str) -> Path:
        """
        缓存文件到本地
        
        Args:
            file_path: 相对文件路径
            content: 文件内容
            
        Returns:
            本地缓存文件路径
        """
        cache_file = self.config_cache_dir / Path(file_path).name
        
        with open(cache_file, 'w', encoding='utf-8') as f:
            f.write(content)
        
        logger.info(f"文件已缓存: {cache_file}")
        return cache_file
    
    def fetch_school_config(self, school_name: str) -> Tuple[Dict[str, Any], str]:
        """
        根据学校名称获取配置文件
        按当前配置源优先级尝试，然后fallback到其他源
        
        Args:
            school_name: 学校名称，如"测试学校"
            
        Returns:
            tuple: (JSON配置字典, Docker Compose文件路径)
        """
        logger.info(f"开始获取学校配置: {school_name}")
        
        # 1. 优先尝试GitHub + jsDelivr CDN
        if self.source_type == "github":
            try:
                return self._fetch_remote_config(school_name)
            except Exception as github_error:
                logger.warning(f"GitHub + jsDelivr获取失败: {github_error}")
                
                # 2. 备用方案：尝试Gitee
                try:
                    gitee_service = ConfigService(source_type="gitee")
                    return gitee_service._fetch_remote_config(school_name)
                except Exception as gitee_error:
                    logger.warning(f"Gitee获取失败: {gitee_error}")
                    
                    # 3. 最后备用方案：使用本地配置文件
                    try:
                        return self._fetch_local_config(school_name)
                    except Exception as local_error:
                        logger.error(f"本地配置也失败: {local_error}")
                        raise Exception(f"无法获取学校配置 {school_name}: GitHub={github_error}, Gitee={gitee_error}, 本地={local_error}")
        
        else:
            # 如果当前是Gitee模式，按相应优先级处理
            try:
                return self._fetch_remote_config(school_name)
            except Exception as gitee_error:
                logger.warning(f"Gitee配置获取失败: {gitee_error}")
                
                # 备用方案：使用本地配置文件
                try:
                    return self._fetch_local_config(school_name)
                except Exception as local_error:
                    logger.error(f"本地配置也失败: {local_error}")
                    raise Exception(f"无法获取学校配置 {school_name}: Gitee={gitee_error}, 本地={local_error}")
    
    def _fetch_remote_config(self, school_name: str) -> Tuple[Dict[str, Any], str]:
        """从远程源获取配置"""
        # 直接使用LWX平台返回的学校名称，如"测试学校"
        logger.info(f"🏫 [CONFIG] 使用学校名称: {school_name}")
        
        # 获取JSON配置文件: config/{school_name}.json
        json_file_path = f"config/{school_name}.json"
        logger.info(f"📄 [CONFIG] 拉取JSON配置: {json_file_path}")
        try:
            json_content = self._get_file_content(json_file_path)
            json_config = json.loads(json_content)
            logger.info(f"✅ [CONFIG] JSON配置拉取成功: {json_file_path}")
        except Exception as e:
            logger.error(f"❌ [CONFIG] JSON配置拉取失败: {school_name} - {e}")
            raise Exception(f"获取JSON配置失败: config/{school_name}.json - {e}")
        
        # 缓存JSON配置
        cached_json = self._cache_file(json_file_path, json_content)
        
        # 获取Docker Compose文件: config/docker-compose-{school_name}.yml
        compose_file_path = f"config/docker-compose-{school_name}.yml"
        logger.info(f"📄 [CONFIG] 拉取Compose配置: {compose_file_path}")
        try:
            compose_content = self._get_file_content(compose_file_path)
            logger.info(f"✅ [CONFIG] Compose配置拉取成功: {compose_file_path}")
        except Exception as e:
            logger.error(f"❌ [CONFIG] Compose配置拉取失败: docker-compose-{school_name}.yml - {e}")
            raise Exception(f"获取Compose配置失败: config/docker-compose-{school_name}.yml - {e}")
        
        # 缓存Compose文件
        cached_compose = self._cache_file(compose_file_path, compose_content)
        
        logger.info(f"🎉 [CONFIG] 学校配置拉取完成: {school_name}")
        logger.info(f"📁 [CONFIG] JSON文件: {json_file_path}")
        logger.info(f"📁 [CONFIG] Compose文件: {compose_file_path}")
        
        return json_config, str(cached_compose)
    
    def _fetch_local_config(self, school_name: str) -> Tuple[Dict[str, Any], str]:
        """从本地文件获取配置（备用方案）"""
        logger.info(f"📁 [LOCAL] 尝试本地配置文件: {school_name}")
        
        # 本地配置文件路径，直接使用学校名称
        local_config_base = Path('/home/mobox/mobox_dashboard/config')
        json_file = local_config_base / f"{school_name}.json"
        compose_file = local_config_base / f"docker-compose-{school_name}.yml"
        
        logger.info(f"📄 [LOCAL] 检查JSON文件: {json_file}")
        if not json_file.exists():
            raise Exception(f"本地配置文件不存在: {json_file}")
        
        logger.info(f"📄 [LOCAL] 检查Compose文件: {compose_file}")
        if not compose_file.exists():
            raise Exception(f"本地Docker Compose文件不存在: {compose_file}")
        
        # 读取JSON配置
        with open(json_file, 'r', encoding='utf-8') as f:
            json_config = json.load(f)
        logger.info(f"✅ [LOCAL] JSON配置读取成功: {json_file}")
        
        # 复制到缓存目录
        cached_compose = self.config_cache_dir / f"docker-compose-{school_name}.yml"
        
        import shutil
        shutil.copy2(compose_file, cached_compose)
        logger.info(f"✅ [LOCAL] Compose文件缓存成功: {cached_compose}")
        
        logger.info(f"🎉 [LOCAL] 本地配置使用成功: {school_name}")
        return json_config, str(cached_compose)
    
    def get_available_schools(self) -> list:
        """
        获取可用的学校列表
        
        Returns:
            学校名称列表
        """
        try:
            # 获取schools目录内容
            url = f"{self.api_base_url}/contents/schools"
            params = {}
            if self.access_token:
                params['access_token'] = self.access_token
            
            response = requests.get(url, params=params, timeout=30)
            response.raise_for_status()
            
            files = response.json()
            schools = []
            
            for file_info in files:
                if file_info['type'] == 'file' and file_info['name'].endswith('.json'):
                    # 提取学校名称（去掉.json后缀）
                    school_name = file_info['name'][:-5]
                    schools.append(school_name)
            
            logger.info(f"找到可用学校: {len(schools)}个")
            return schools
            
        except Exception as e:
            logger.error(f"获取学校列表失败: {e}")
            return []
    
    def validate_school_config(self, config: Dict[str, Any]) -> list:
        """
        验证学校配置的有效性
        
        Args:
            config: JSON配置字典
            
        Returns:
            错误信息列表，空列表表示验证通过
        """
        errors = []
        
        # 检查必要字段
        required_fields = ['images', 'registry', 'school_info']
        for field in required_fields:
            if field not in config:
                errors.append(f"缺少必要字段: {field}")
        
        # 检查镜像配置
        if 'images' in config:
            images = config['images']
            if not isinstance(images, list) or len(images) == 0:
                errors.append("images字段必须是非空数组")
            else:
                for i, image in enumerate(images):
                    if not isinstance(image, dict):
                        errors.append(f"镜像配置[{i}]必须是对象")
                        continue
                    
                    required_image_fields = ['name', 'tag', 'enabled']
                    for field in required_image_fields:
                        if field not in image:
                            errors.append(f"镜像配置[{i}]缺少字段: {field}")
        
        # 检查注册表配置
        if 'registry' in config:
            registry = config['registry']
            if not isinstance(registry, dict):
                errors.append("registry字段必须是对象")
            else:
                required_registry_fields = ['url', 'namespace']
                for field in required_registry_fields:
                    if field not in registry:
                        errors.append(f"registry配置缺少字段: {field}")
        
        return errors
    
    def update_local_config(self, school_name: str, config: Dict[str, Any]) -> bool:
        """
        更新本地配置文件（可选功能）
        
        Args:
            school_name: 学校名称
            config: 更新后的配置
            
        Returns:
            更新是否成功
        """
        try:
            local_config_path = self.config_cache_dir / f"{school_name}.json"
            with open(local_config_path, 'w', encoding='utf-8') as f:
                json.dump(config, f, indent=2, ensure_ascii=False)
            
            logger.info(f"本地配置已更新: {school_name}")
            return True
            
        except Exception as e:
            logger.error(f"更新本地配置失败: {e}")
            return False


def create_gitee_config_service() -> ConfigService:
    """创建配置服务实例，优先使用Gitee"""
    return ConfigService(source_type="gitee")

def create_github_config_service() -> ConfigService:
    """创建GitHub配置服务实例"""
    return ConfigService(source_type="github")

# 保留旧名称的兼容性
GiteeConfigService = ConfigService